Bedrockを活用したブログレビューAPIをローコードで実装してみた
リテールアプリ共創部@大阪の岩田です。
先日こんなブログを書きました。
最初は上記のシステムでメディアポリシー準拠をチェックしているだけだったのですが、ここにタイポチェックも組み込めないかという要望を頂き機能追加をすることになりました。さらにその後ブログ公開後にPULL型でレビューするのではなく、ブログ公開前にAIによる自動レビューを通せないか?という要望も頂きました。
ブログ公開前のレビューについては著者が内容をコピペしてレビュー依頼のプロンプトを書けば実現できる話ではあるのですが、ブラウザのタブを行き来したり、プロンプトを試行錯誤したりする手間を考えるとブログ執筆環境にレビューボタンのようなものが統合されていた方が便利です。ということでブログ執筆環境を整えるためのバックエンドAPIを実装してみました。
構成
今回作成するシステムの概要です。ざっくりこんな構成を作ります。
- API Gatewayのサービス統合でStep FunctionsのExpress Workflowを起動
- Step FunctionsのWorkflowはParallelステートとサービス統合を利用して以下を実行
- Bedrockにメディアポリシー準拠のレビューを依頼
- Bedrockにタイポチェックを依頼
実装
ここからは実装を紹介していきます。各種リソースはSAMを使って定義しています。
まずSAMテンプレートです。
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Parameters:
Environment:
Type: String
Default: dev
AllowedValues:
- dev
- stg
- prd
Resources:
Api:
Type: AWS::Serverless::Api
Properties:
StageName: !Ref Environment
DefinitionBody:
Fn::Transform:
Name: AWS::Include
Parameters:
Location: oas.yaml
Auth:
UsagePlan:
CreateUsagePlan: PER_API
Description: !Sub ${Environment} ブログ自動レビューAPI用使用料プラン
Quota:
Limit: 1000
Period: DAY
Throttle:
RateLimit: 10
BurstLimit: 20
UsagePlanName: !Sub ${Environment}-blog-review-usage-plan
ApiGwRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- apigateway.amazonaws.com
Action:
- sts:AssumeRole
Path: /
Policies:
- PolicyName: sfn-integration
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- states:StartSyncExecution
Resource: '*'
BlogReviewStateMachine:
Type: AWS::Serverless::StateMachine
Properties:
Name: !Sub "${Environment}-blog-review"
Type: EXPRESS
DefinitionUri: blog-review.asl.yaml
DefinitionSubstitutions:
Environment: !Ref Environment
Role: !GetAtt BlogReviewStateMachineRole.Arn
Tracing:
Enabled: true
Logging:
Level: ALL
IncludeExecutionData: True
Destinations:
- CloudWatchLogsLogGroup:
LogGroupArn: !GetAtt BlogReviewStateMachineLog.Arn
BlogReviewStateMachineLog:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName : !Sub "/aws/states/${Environment}-blog-review-api"
RetentionInDays: 30
BlogReviewStateMachineRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- states.amazonaws.com
Action:
- sts:AssumeRole
Path: /
ManagedPolicyArns:
- arn:aws:iam::aws:policy/CloudWatchLogsFullAccess
Policies:
- PolicyName: bedrock
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- bedrock:InvokeModel
Resource: '*'
あまり大したことはやっていなくて、API GWやIAMロールを作成しています。どちらかというと別ファイルに切り出しているステートマシンの定義とOpen APIの定義が重要です。
ステートマシンを定義しているblog-review.asl.yaml
は以下の通りです。
---
Comment: (${Environment}) DevIOのブログをレビューするステートマシン
StartAt: Parallel
States:
Parallel:
Type: Parallel
End: true
Branches:
- StartAt: ReviewBlogMediaPolicy
States:
ReviewBlogMediaPolicy:
Type: Task
Resource: arn:aws:states:::bedrock:invokeModel
Parameters:
ModelId: arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-3-haiku-20240307-v1:0
Body:
anthropic_version: bedrock-2023-05-31
max_tokens: 2000
system: |
あなたは企業ブログのレビュワーです
メディアポリシーに従ってブログ内に不適切な表現がないかチェックする必要があります。
...略
messages:
- role: user
content:
- type: text
text.$: $.article
temperature: 0
OutputPath: $.Body.content[0].text
End: true
- StartAt: ReviewBlogTypo
States:
ReviewBlogTypo:
Type: Task
Resource: arn:aws:states:::bedrock:invokeModel
Parameters:
ModelId: arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-3-5-sonnet-20240620-v1:0
Body:
anthropic_version: bedrock-2023-05-31
max_tokens: 2000
system: |
あなたは企業ブログのレビュワーです
...略
messages:
- role: user
content:
- type: text
text.$: $.article
temperature: 0
OutputPath: $.Body.content[0].text
End: true
ResultSelector:
mediaPolicy.$: States.ArrayGetItem($,0)
typo.$: States.ArrayGetItem($,1)
Bedrockのモデルやプロンプトは様子を見ながら適宜調整する想定です。
ステートマシンへの入力は以下のような入力を期待しています,。
{
"article": "レビュー対象となる記事の内容"
}
Bedrockにレビューを経て最終的な出力はParallel
ステートのResultSelector
の指定により以下のような形式となります。
{
"mediaPolicy":"このブログに特に問題はありませんでした",
"typo":"タイポは特に見つかりませんでした"
}
作成されるステートマシンの定義は以下のようになります。
oas.yaml
の中身は以下の通りです。
openapi: 3.0.1
info:
title:
Fn::Sub: ${Environment} Blog Review API
version: '1.0'
x-amazon-apigateway-request-validators:
body-only:
validateRequestBody: true
validateRequestParameters: false
x-amazon-apigateway-request-validator: body-only
paths:
/review:
post:
security:
- api_key: []
requestBody:
content:
application/json:
schema:
type: object
properties:
article:
type: string
# 1文字だけレビューしても有効なレビューはもらえないので、本来はもっと桁数を大きくした方が良い
minLength: 1
# TODO maxLengthも設定する
nullable: false
required:
- article
responses:
'200':
description: Review created
content:
application/json:
schema:
type: object
properties:
typo:
type: string
example: タイポ警察だ!
mediaPolicy:
type: string
example: 特に問題ありません
x-amazon-apigateway-integration:
credentials:
Fn::GetAtt: ApiGwRole.Arn
httpMethod: "POST"
uri:
Fn::Sub: "arn:aws:apigateway:${AWS::Region}:states:action/StartSyncExecution"
responses:
200:
statusCode: "200"
responseTemplates:
application/json: |
#set($inputRoot = $input.path('$'))
#if($input.path('$.status').toString().equals("FAILED"))
#set($context.responseOverride.status = 500)
{
"error": "$input.path('$.error')",
"cause": "$input.path('$.cause')"
}
#else
$input.path('$.output')
#end
requestTemplates:
application/json:
Fn::Sub:
- |
#set($inputRoot = $input.path('$'))
{
"input": "$util.escapeJavaScript($input.json('$')).replaceAll("\\'","'")",
"stateMachineArn": "${BlogReviewStateMachineArn}"
}
- BlogReviewStateMachineArn:
Fn::GetAtt: BlogReviewStateMachine.Arn
passthroughBehavior: "when_no_templates"
type: "aws"
components:
securitySchemes:
api_key:
type: "apiKey"
name: "x-api-key"
in: "header"
ポイントをいくつか紹介します。
security
にはapi_key
を指定してAPIキーの利用を必須としています。SAMテンプレートで以下のように使用料プランを指定しており、この使用料プランに紐づくAPIキーを利用してもらう想定です。何度もAPIを呼び出されるとBedrockの利用料が高く付いてしまうので...。
Quota:
Limit: 1000
Period: DAY
Throttle:
RateLimit: 10
BurstLimit: 20
x-amazon-apigateway-request-validator
にはx-amazon-apigateway-request-validators
で指定したバリデータを指定しており、リクエストボディをAPI GWにバリデーションさせます。これによってリクエストボディが空のJSONの場合や、article
が空文字列の場合はAPI GWから400エラーが返却されることになります。
x-amazon-apigateway-integration
配下のrequestTemplates
では統合リクエストのマッピングテンプレートを指定しています。このテンプレートは以下のようなVTLテンプレートです。
#set($inputRoot = $input.path('$'))
{
"input": "$util.escapeJavaScript($input.json('$')).replaceAll("\\'","'")",
"stateMachineArn": "arn:aws:states:us-east-1:123456789012:stateMachine:dev-blog-review"
}
ブログ記事には改行コード等が含まれるため$util.escapeJavaScript
を使って記事をエスケープします。さらにシングルクォーテーションをうまく扱えるようにreplaceAll("\\'","'")
でシングルクォーテーションもエスケープします。
これは公式ドキュメントの記載に従って記述しています。
注記
この関数は、通常の一重引用符 (
'
) をエスケープした一重引用符 (\'
) に変換します。ただし、エスケープした一重引用符は JSON で有効ではありません。したがって、この関数からの出力を JSON のプロパティで使用する場合、エスケープした一重引用符 (\'
) を通常の一重引用符 ('
) に戻す必要があります。
API Gateway マッピングテンプレートとアクセスのログ記録の変数リファレンス - Amazon API Gateway
参考までにCDKのStepFunctionsExecutionIntegration
も同様のVTLテンプレートを作成するように実装されています。
動作確認
SAMテンプレートの準備ができたのでsam build
とsam deploy
でリソースを一式デプロイし、実際にAPIを叩いて動作確認してみます。
curl 'https://<API GWのID>.execute-api.us-east-1.amazonaws.com/dev/review' \
--header 'x-api-key: APIキー' \
--header 'Content-Type: application/json' \
--data '{
"article":"ブログの下書きをAIにレビューしてもらうAPIをローコードで実装してみた"
}'
以下のようなレスポンスが返却されてきました。
{
"typo": "レビューの結果、以下の点を指摘いたします:\n\n1. タイポ:\n 特に見当たりません。\n\n2. 日本語として不自然な言い回し:\n 特に見当たりません。\n\n3. 助詞の使い方:\n 特に不自然な点は見当たりません。\n\n全体的に、タイトルは簡潔で分かりやすく、日本語として自然な表現が使われています。助詞の使用も適切です。",
"mediaPolicy": "ブログの下書きを確認しました。全体的に適切な内容だと思いますが、以下の点について改善をご検討ください。...略\n\nその他、特に問題となる箇所は見当たりませんでした。メディアポリシーに沿った適切な内容だと評価します。\nご検討ください。"
}
期待通りのレスポンスが返却されました!
まとめ
ブログの下書きをAIにレビューしてもらうAPIをローコードで実装してみました。Lambda無しで簡単にAPIを作れることが分かると思います。このAPIを各ブログ著者の執筆環境と統合して呼び出せるようにVS CodeのExtension等を作りこめば快適な執筆環境が手に入りそうです。
今回APIの認証はAPIキーによる簡易な認証としていますが、公式ドキュメントにも記載されている通りAPIキーだけで認証・認可を行うのは推奨されません。
API キーを、API へのアクセスを制御するための認証または承認に使用しないでください
API Gateway での REST API の使用量プランと API キー - Amazon API Gateway
別途Cognito等を利用してAPI認証・認可を設定するのが良さそうです。ただし、Cognito等で認証・認可する場合もレートリミットをかけるためにAPIキーと使用量プランは利用した方が良いでしょう。